import {
  Body,
  Controller,
  Delete,
  ForbiddenException,
  Get,
  NotFoundException,
  Param,
  Patch,
  Post,
  UnprocessableEntityException,
  UploadedFiles,
  UseGuards,
  UseInterceptors
} from '@nestjs/common'
import { ItemsService } from './items.service'
import { CreateItemDto } from './dto/create-item.dto'
import { UpdateItemDto } from './dto/update-item.dto'
import {
  Crud,
  CrudController,
  CrudRequest,
  Override,
  ParsedBody,
  ParsedRequest
} from '@nestjsx/crud'
import { Item } from './entities/item.entity'
import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'
import { Public } from 'src/auth/decorators/public.decorator'
import { CurrentUser } from 'src/common/user.decorator'
import { User } from 'src/users/entities/user.entity'
import { FilesInterceptor } from '@nestjs/platform-express'
import { Express } from 'express'
import { extname } from 'path'
import { diskStorage } from 'multer'
import { randomUUID } from 'crypto'
import { Photo } from './entities/photo.entity'
import { Repository } from 'typeorm'
import { InjectRepository } from '@nestjs/typeorm'
import { Comment, TargetType } from 'src/users/entities/comment.entity'
import { CreateCommentDto } from './dto/create-comment.dto'
import { SetFavoriteDto } from './dto/set-favorite.dto'
import { Favorite } from 'src/users/entities/favorite.entity'
import { History } from 'src/users/entities/history.entity'

@Crud({
  model: {
    type: Item
  },
  dto: {
    create: CreateItemDto,
    update: UpdateItemDto
  },
  query: {
    join: {
      user: {
        eager: true,
        allow: ['id']
      },
      'user.profile': {
        eager: true,
        allow: ['nickname', 'avatar', 'gender']
      },
      photos: {
        eager: true,
        allow: ['id', 'path', 'order']
      }
    }
  },
  routes: {
    getOneBase: {
      decorators: [Public()]
    }
  }
})
@UseGuards(JwtAuthGuard)
@Controller('api/items')
export class ItemsController implements CrudController<Item> {
  constructor(
    public service: ItemsService,
    // TODO: cleanup
    @InjectRepository(Photo)
    public photosRepository: Repository<Photo>,
    @InjectRepository(Comment)
    public commentsRepository: Repository<Comment>,
    @InjectRepository(Favorite)
    public favoritesRepository: Repository<Favorite>,
    @InjectRepository(History)
    public historiesRepository: Repository<History>
  ) {}

  get base(): CrudController<Item> {
    return this
  }

  @Override()
  @Public()
  async getMany(@ParsedRequest() req: CrudRequest) {
    // TODO: 在其他接口上也过滤其他人还未发布的商品（能查看自己发布的所有商品）
    // req.parsed.search.$and = req.parsed.search.$and ?? []
    // req.parsed.search.$and.push({
    //   status: { $ne: ItemStatus.DRAFT }
    // })
    return await this.base.getManyBase?.(req)
  }

  @Override()
  async createOne(
    @ParsedRequest() req: CrudRequest,
    @ParsedBody() dto: CreateItemDto,
    @CurrentUser() user: User
  ) {
    dto.user = user
    return await this.base.createOneBase?.(req, dto as Item)
  }

  // TODO: 不允许随意修改商品状态
  // @Override() async updateOne

  // TODO: 考虑安全漏洞
  @Post(':id/photos')
  @UseInterceptors(
    FilesInterceptor('files', 10, {
      storage: diskStorage({
        destination: './public/uploads/items',
        filename: (req, file, cb) => {
          // TODO: 使用 SHA-1 hash 作为文件名
          const filename = randomUUID() + extname(file.originalname)
          return cb(null, filename)
        }
      })
    })
  )
  async uploadItemPhotos(
    @Param('id') id: string,
    @CurrentUser() user: User,
    @UploadedFiles() files: Express.Multer.File[]
  ) {
    const item = await this.service.repo.findOneOrFail(id, {
      relations: ['user']
    })

    if (item.user.id !== user.id) {
      throw new ForbiddenException()
    }

    if (item.photos.length >= 10) {
      throw new UnprocessableEntityException('每件商品最多只能上传 10 张图片')
    }

    // TODO: 业务逻辑是否应该移动至 service 中？
    let order = item.photos.length || 0
    const photos: Array<Partial<Photo>> = files.map((file) => {
      return {
        id: file.filename.split('.')[0],
        path: file.path,
        order: ++order,
        item
      }
    })

    await this.photosRepository.save(photos)
    return await this.photosRepository.find({ where: { item } })
  }

  @Delete(':id/photos')
  async deleteAllItemPhotos(
    @Param('id') id: string,
    @CurrentUser() user: User
  ) {
    // TODO: check resource owner
    const item = await this.service.repo.findOneOrFail(id)
    return await this.photosRepository.softRemove(item.photos)
  }

  @Delete(':id/photos/:pid')
  async deleteOneItemPhoto(
    @Param('id') itemId: string,
    @Param('pid') photoId: string,
    @CurrentUser() user: User
  ) {
    // TODO: check resource owner
    const photo = await this.photosRepository.findOne({
      where: {
        id: photoId,
        item: {
          id: itemId
        }
      }
    })

    if (!photo) {
      throw new NotFoundException()
    }

    return await this.photosRepository.softRemove(photo)
  }

  @Get(':id/comments')
  async getAllComments(@Param('id') id: string) {
    const item = await this.service.repo.findOneOrFail(id)

    return await this.commentsRepository.find({
      where: {
        targetId: item.id,
        targetType: TargetType.ITEM
      },
      relations: ['user']
    })
  }

  @Post(':id/comments')
  async createOneComment(
    @Param('id') id: string,
    @Body() dto: CreateCommentDto,
    @CurrentUser() user: User
  ) {
    const item = await this.service.repo.findOneOrFail(id)

    return await this.commentsRepository.save({
      targetId: item.id,
      targetType: TargetType.ITEM,
      content: dto.content,
      user
    })
  }

  async queryItemUserFavorite(item: Item, user: User) {
    return await this.favoritesRepository
      .createQueryBuilder('favorite')
      .where({
        item: {
          id: item.id
        },
        user: {
          id: user.id
        }
      })
      .select(['user.id', 'item.id', 'favorite'])
      .leftJoin('favorite.item', 'item')
      .leftJoin('favorite.user', 'user')
      .getOne()
  }

  @Get(':id/favorite')
  async isFavorite(@Param('id') id: string, @CurrentUser() user: User) {
    const item = await this.service.repo.findOneOrFail(id)
    const query = await this.queryItemUserFavorite(item, user)

    return {
      favorite: query?.favorite ?? false
    }
  }

  @Patch(':id/favorite')
  async setFavorite(
    @Param('id') id: string,
    @Body() dto: SetFavoriteDto,
    @CurrentUser() user: User
  ) {
    const item = await this.service.repo.findOneOrFail(id)
    const query = await this.queryItemUserFavorite(item, user)
    let result: Favorite

    if (query) {
      query.favorite = dto.favorite
      result = await this.favoritesRepository.save(query)
    } else {
      result = await this.favoritesRepository.save({
        item,
        user,
        favorite: dto.favorite
      })
    }

    return {
      favorite: result.favorite
    }
  }

  async queryItemUserHistory(item: Item, user: User) {
    return await this.historiesRepository
      .createQueryBuilder('history')
      .where({
        item: {
          id: item.id
        },
        user: {
          id: user.id
        }
      })
      .select(['user.id', 'item.id', 'history'])
      .leftJoin('history.item', 'item')
      .leftJoin('history.user', 'user')
      .getOne()
  }

  @Patch(':id/history')
  async updateHistory(@Param('id') id: string, @CurrentUser() user: User) {
    const item = await this.service.repo.findOneOrFail(id)
    const history = await this.queryItemUserHistory(item, user)
    let result: History

    if (history) {
      history.count += 1
      result = await this.historiesRepository.save(history)
    } else {
      result = await this.historiesRepository.save({
        item,
        user,
        count: 1
      })
    }

    return {
      count: result.count
    }
  }

  @Delete(':id/history')
  async deleteHistory(@Param('id') id: string, @CurrentUser() user: User) {
    const item = await this.service.repo.findOneOrFail(id)
    const history = await this.queryItemUserHistory(item, user)

    if (!history) {
      throw new NotFoundException()
    }

    return await this.historiesRepository.softRemove(history)
  }
}
